package com.parleys.service;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Bitmap.CompressFormat;
import android.util.Log;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.parleys.domain.Presentation;
import com.parleys.helper.ApplicationConstants;
import com.parleys.helper.MP3DownloadCallback;
import com.parleys.helper.SoftHashMap;
import com.parleys.service.intf.ParleysHTTPClient;
import com.parleys.service.intf.PresentationAssetsService;

@Singleton
public class PresentationAssetsServiceImpl implements PresentationAssetsService {

	private static final Map<String, Bitmap> IMAGE_CACHE 
		= Collections.synchronizedMap(new SoftHashMap<String, Bitmap>(0));

	private static final String TAG = PresentationAssetsServiceImpl.class.getName();

	private static Boolean IS_DIRECTORY_EXISTANCE_CHECKED = false;

	private ParleysHTTPClient parleysHTTPClient;

	@Inject
	public PresentationAssetsServiceImpl(final ParleysHTTPClient parleysHTTPClient) {
		this.parleysHTTPClient = parleysHTTPClient;
	}

	public Bitmap getImage(final String assetBaseUrl, final String type, final Integer objectId, final String asset) {
		Bitmap bm = IMAGE_CACHE.get(objectId + asset);
		InputStream instream = null;

		if (bm != null) {
			return bm;
		}

		try {
			// checks if the file has changed the last second.
			// If so recheck if it appears in the map every 50ms
			File file = getStorageFile(objectId, asset);
			while (file.lastModified() + 1000 > System.currentTimeMillis()) {
				Log.d(TAG, "In wait lock");
				try {
					Thread.sleep(50);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				bm = IMAGE_CACHE.get(objectId + asset);
				if (bm != null) {
					return bm;
				}
			}
//			Log.w(TAG, "Native heap size: " + Debug.getNativeHeapAllocatedSize() / 1024 + "kb / "
//					+ Debug.getNativeHeapSize() / 1024 + "kb");
			bm = BitmapFactory.decodeFile(file.getAbsolutePath(), buildBitmapFactoryOptions());
			Log.d(TAG, "Retrieving image from file");
			if (bm != null) {
				IMAGE_CACHE.put(objectId + asset, bm);
				return bm;
			}
		} finally {
			IOUtils.closeQuietly(instream);
		}

		return downloadImage(assetBaseUrl, type, objectId, asset);

	}

	private Bitmap downloadImage(final String assetBaseURL, final String type, final Integer objectId,
			final String asset) {

		Bitmap bm = null;

		StringBuilder builder = new StringBuilder(512);
		builder.append(assetBaseURL);
		builder.append(type);
		builder.append(ApplicationConstants.SLASH);
		builder.append(objectId);
		builder.append(ApplicationConstants.SLASH);
		builder.append(asset.replace("/", ""));

		InputStream instream = null;
		OutputStream outstream = null;

		try {
			instream = (InputStream) parleysHTTPClient.getContent(builder.toString()).getContent();
			// passing an inputstream directly (decodeInputStream) seems to leak more memory. 
			// So we convert to byte array first
			byte[] data = IOUtils.toByteArray(instream);
			bm = BitmapFactory.decodeByteArray(data, 0, data.length, buildBitmapFactoryOptions());
			outstream = new BufferedOutputStream(new FileOutputStream(getStorageFile(objectId, asset)),
					ApplicationConstants.STREAM_BUFFER_SIZE);

			if (bm != null) {
				Log.i(TAG, "Writing image to file");
				bm.compress(CompressFormat.JPEG, 70, outstream);
				IMAGE_CACHE.put(objectId + asset, bm);
			}
		} catch (FileNotFoundException e) {
			Log.i(TAG, e.toString());
		} catch (ClientProtocolException e) {
			Log.i(TAG, e.toString());
		} catch (IOException e) {
			Log.i(TAG, e.toString());
		} finally {
			IOUtils.closeQuietly(instream);
			IOUtils.closeQuietly(outstream);
		}

		return bm;
	}

	private File getStorageFile(final Integer presentationId, final String asset) {
		if (!IS_DIRECTORY_EXISTANCE_CHECKED) {
			ApplicationConstants.EXTERNAL_STORAGE_DIRECTORY.mkdir();
			ApplicationConstants.ASSETS_DIRECTORY.mkdir();
		}
		return new File(ApplicationConstants.ASSETS_DIRECTORY, presentationId
				+ asset.replace(ApplicationConstants.SLASH, ""));
	}

	public void downloadMP3(final String mediaBaseURL, final Presentation presentation, final MP3DownloadCallback callback) {
		OutputStream outstream = null;
		InputStream instream = null;

		try {
			HttpEntity httpEntity = parleysHTTPClient.getContent(mediaBaseURL + presentation.getId()
					+ presentation.getMp3());
			instream = httpEntity.getContent();
			outstream = new BufferedOutputStream(new FileOutputStream(getStorageFile(presentation.getId(), presentation
					.getMp3())), ApplicationConstants.STREAM_BUFFER_SIZE);

			Long totalDownloaded = 0l;
			Long totalToDownload = httpEntity.getContentLength();
			byte[] buffer = new byte[ApplicationConstants.STREAM_BUFFER_SIZE * 50];
			int n = 0;
			int loopCounter = 0;
			while (-1 != (n = instream.read(buffer))) {
				outstream.write(buffer, 0, n);
				totalDownloaded += n;

				// dont call callback too often
				if (callback != null && loopCounter % 10 == 0) {
					callback.updateProgress(totalDownloaded, totalToDownload);
				}
				loopCounter++;
			}

			// call callback when download finished
			if (callback != null) {
				callback.updateProgress(totalDownloaded, totalToDownload);
			}
		} catch (IOException e1) {
			e1.printStackTrace();
		} finally {
			IOUtils.closeQuietly(instream);
			IOUtils.closeQuietly(outstream);
		}

	}

	@Override
	public Boolean isImageCached(Integer objectId, String asset) {
		if (IMAGE_CACHE.get(objectId + asset) != null) {
			return true;
		} else {
			return getStorageFile(objectId, asset).exists();
		}
	}

	/**
	 * This method produces an instance of Bitmap.Options with inPurgeable & inInputShareable set to true. These
	 * properties prevent the massive memory leaks on the native heap that occur when decoding lots of files, byte
	 * arrays & inpustreams to Bitmap instances We are using reflection to set these properties so we can remain Android
	 * 1.5 compatible (member variables do not exist in 1.5)
	 * 
	 * @return Bitmap.Options instance to be used when doing image operations
	 */
	public BitmapFactory.Options buildBitmapFactoryOptions() {

		BitmapFactory.Options ops = new BitmapFactory.Options();
		try {
			Field f = ops.getClass().getField("inPurgeable");
			f.setBoolean(ops, Boolean.TRUE);

			f = ops.getClass().getField("inInputShareable");
			f.setBoolean(ops, Boolean.TRUE);

		} catch (Throwable t) {
			// t.printStackTrace();
		}
		return ops;

	}

}
